﻿Public NotInheritable Class PackagedFileStream
    Inherits System.IO.Stream
    ' | Arps Engine Streamed Reader                                         |
    ' | Package DATABLOCK RAW Streamed Reader                               |
    ' | Supports Reading of data stored in DATABLOCKS via custom Streams    |
    ' | By KDERazorback                                                     |
    ' | Project Started: 24/04/2011                                         |
    ' +=====================================================================+

    Private _filename As String
    ''' <summary>
    ''' Stores the Filename of the data file currently Loaded
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks>This only stores the File Name, excluding the Path of the File</remarks>
    Public ReadOnly Property Filename As String
        Get
            Return _filename
        End Get
    End Property
    Private _directory As String
    ''' <summary>
    ''' Stores the directory on wich the File is stored
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks>This returns the Path only without the File name</remarks>
    Public ReadOnly Property Directory As String
        Get
            Return _directory
        End Get
    End Property
    Private _extension As String
    ''' <summary>
    ''' Return the File Extension of the file loaded
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks>Only returns the Extension part of the Filename, without the Dot. If the file dont have extension, this field will be empty</remarks>
    Public ReadOnly Property Extension As String
        Get
            Return _extension
        End Get
    End Property
    Private _fullName As String
    ''' <summary>
    ''' Returns the Full path of the File. Including Filename, Path, and Extension
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property FullName As String
        Get
            Return _fullName
        End Get
    End Property
    Private _length As Long
    ''' <summary>
    ''' Stores the size of the File in bytes
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Overrides ReadOnly Property Length As Long
        Get
            Return _length
        End Get
    End Property
    Private _extendedParameters() As String
    ''' <summary>
    ''' Stores an array of strings in the form of Script Combos that represent any Extended Parameter that may be explicitly specified by the Package
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property ExtendedParameters As String()
        Get
            Return _extendedParameters
        End Get
    End Property
    Private _md5Hash As String
    ''' <summary>
    ''' Stores the MD5 Sum of the File loaded
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks>The MD5 is stored as a string of 16 two digits Hexadecimal values in Uppercase</remarks>
    Public ReadOnly Property Md5Hash As String
        Get
            Return _md5Hash
        End Get
    End Property
    Private _packagePath As String
    Private _packageOffset As ULong
    Private _internalSize As ULong
    Private _packageFlags As PackageDescriptorFlags
    Private _cached As Boolean
    ''' <summary>
    ''' Stores a flag that specify if the current file has been Cached to disk in order to reduce Package I/O, and CPU time
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property Cached As Boolean
        Get
            Return _cached
        End Get
    End Property
    Friend _cachedCopyPath As String
    Private _position As Long
    ''' <summary>
    ''' Stores the position of the Stream head inside the loaded File. This points to the byte that will be readed when calling a Read method
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Overrides Property Position As Long
        Get
            Return _position
        End Get
        Set(ByVal newPosition As Long)
            SeekTo(newPosition, False)
        End Set
    End Property

    Private _closed As Boolean
    ''' <summary>
    ''' Stores a value indicating if this instance of PackagedFileStream has been closed
    ''' </summary>
    ''' <value></value>
    ''' <returns>True if the stream has already been closed, false if not</returns>
    ''' <remarks></remarks>
    Public ReadOnly Property Closed As Boolean
        Get
            Return _closed
        End Get
    End Property

    ' Real Stream
    Private _ioStream As IO.FileStream
    Private _ioReader As IO.BinaryReader

    ''' <summary>
    ''' Sets the current position of the Head inside the file
    ''' </summary>
    ''' <param name="offset">Value that indicates where to put the Header</param>
    ''' <param name="relative">Value that specify if the Offset is relative to the current position, if not, its an Absolute Offset</param>
    ''' <remarks></remarks>
    Public Sub SeekTo(ByVal offset As ULong, ByVal relative As Boolean)
        If _length = 0 Then Exit Sub

        If relative = True Then
            ' Relative to current position
            If offset = 0 Then Exit Sub
            Dim newOffset As ULong
            newOffset = _packageOffset + _position + offset
            If (newOffset > (_packageOffset + _internalSize)) Or (newOffset < _packageOffset) Then
                Err.Raise(vbObjectError + 513, , "Offset out of Bounds")
                Exit Sub
            End If
            ' Valid offset
            _ioStream.Seek(newOffset, IO.SeekOrigin.Begin)
            ' Update Position var
            _position = newOffset - _packageOffset
        Else
            ' Absolute to package starting offset
            Dim newOffset As ULong
            newOffset = _packageOffset + offset
            If (newOffset > (_packageOffset + _internalSize)) Or (newOffset < _packageOffset) Then
                Err.Raise(vbObjectError + 513, , "Offset out of Bounds")
                Exit Sub
            End If
            ' Valid offset
            _ioStream.Seek(newOffset, IO.SeekOrigin.Begin)
            ' Update Position var
            _position = newOffset - _packageOffset
        End If
    End Sub
    ''' <summary>
    ''' Sets the current position of the Head inside the file
    ''' </summary>
    ''' <param name="offset">Value that indicates where to put the Header. This offset is Absolute</param>
    ''' <remarks></remarks>
    Public Sub SeekTo(ByVal offset As ULong)
        SeekTo(offset, False)
    End Sub

    ''' <summary>
    ''' Reads one byte from the attached Stream, and move the Head ahead one position
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    ''' 
    Public Overrides Function ReadByte() As Integer
        If CanRead = False Then Return -1
        If _length = 0 Then Return -1

        Dim out() As Byte
        out = ReadBytes(1)

        ' Dont update position since this method uses ReadBytes(), wich already updates it
        '' Update _position
        '_position += 1

        Try
            Return out(0)
        Catch ex As Exception
            Return -1
        End Try
    End Function

    ''' <summary>
    ''' Reads an specified ammount of bytes from the attached Stream, and move the Head ahead that ammount of bytes
    ''' </summary>
    ''' <param name="count">Ammount of bytes that will be readed starting for the Head Location</param>
    ''' <returns></returns>
    ''' <remarks>If count is zero, this mehod returns nothing</remarks>
    Public Function ReadBytes(ByVal count As ULong) As Byte()
        ' Check that the Stream is on the right position

        If count = 0 Then Return Nothing
        If _position = _packageOffset + _internalSize Then Return Nothing
        If _length = 0 Then Return Nothing

        If Not _ioStream.Position = _position + _packageOffset Then
            ' Sync stream with location var
            Seek(_position, False)
        End If

        ' Check that Position + Bytes is still inside the bounds
        If (_position + count) > (_internalSize) Then ' If (_position + count) > (_packageOffset + _internalSize) Then
            ' Tweak count to fit into the Bounds
            Dim overflow As ULong
            overflow = (_position + count) - (_internalSize) ' 4194304 - 576 = 4193728
            count = count - overflow
        End If

        ' Read Data
        Dim buffer() As Byte
        buffer = _ioReader.ReadBytes(count)

        ' Update _position
        _position += count

        Return buffer
    End Function

    ''' <summary>
    ''' Resets the Stream to its initial state, moving the Head to the first byte and deleting any stored Cache
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub Reset()
        _CloseStreams(_ioStream, _ioReader)

        Dim fCollection As Utilities.FilenameCollection
        fCollection.Directory = _directory
        fCollection.Extension = _extension
        fCollection.Filename = _filename
        fCollection.FullFilename = _fullName

        _init(_packagePath, _packageOffset, _internalSize, _packageFlags, _extendedParameters, _md5Hash, fCollection)
    End Sub

    ''' <summary>
    ''' Safely closed this Stream and all of its associated resources
    ''' </summary>
    ''' <remarks>Its important to call this method when this stream is not needed anymore, in order to free any disk cache, and avoid dummy File Locks</remarks>
    Public Overrides Sub Close()
        If Not _length = 0 Then _CloseStreams(_ioStream, _ioReader)
        '_directory = ""
        'Erase _extendedParameters
        '_extension = ""
        '_filename = ""
        '_fullName = ""
        '_internalSize = 0
        '_length = 0
        '_md5Hash = ""
        '_packageFlags = PackageDescriptorFlags.JumpDirectly
        '_packageOffset = 0
        '_packagePath = ""
        _position = 0
        _closed = True

        ' TODO: Clean cache here
    End Sub

    ''' <summary>
    ''' Prepares and Initialized a new instance of this Stream
    ''' </summary>
    ''' <param name="package">Filename of the package that stores the Target File</param>
    ''' <param name="offset">Absolute offset to the Package, to the first byte of the Target file</param>
    ''' <param name="length">Length in Bytes of the Target file</param>
    ''' <param name="flags">Flags that needs to be applied to the DATABLOCK before getting ready for Read</param>
    ''' <param name="extendedParameters">Array of Extended parameters that affects the Target File</param>
    ''' <param name="md5Hash">MD5 Sum of the Target File, BEFORE the flags being applied</param>
    ''' <param name="fCollection">Collection that stores the different components of the Packaged Filename</param>
    ''' <remarks></remarks>
    Friend Sub _init(ByVal package As String, ByVal offset As ULong, ByVal length As ULong, ByVal flags As PackageDescriptorFlags, ByVal extendedParameters() As String, ByVal md5Hash As String, ByVal fCollection As FilenameCollection)
        ' Initializes a new instance of this Stream with the specified information

        _closed = True

        ' Store parameters
        _packagePath = package
        _packageOffset = offset
        _length = length
        _internalSize = length
        _packageFlags = PackageDescriptorFlags.JumpDirectly
        _md5Hash = md5Hash
        _extendedParameters = extendedParameters

        ' Open Package for reading at the desired offsets
        If Not _length = 0 Then
            Try
                _ioStream = New IO.FileStream(package, IO.FileMode.Open, IO.FileAccess.Read, IO.FileShare.Read)
                _ioReader = New IO.BinaryReader(_ioStream)
                _ioStream.Seek(offset, IO.SeekOrigin.Begin)
            Catch ex As Exception
                _CloseStreams(_ioStream, _ioReader)
                Exit Sub
            End Try
        End If

        ' Configure stream parameters
        _position = 0
        _filename = fCollection.Filename
        _fullName = fCollection.FullFilename
        _extension = fCollection.Extension
        _directory = fCollection.Directory

        _closed = False
    End Sub

    Public Sub _CloseStreams(ByVal stream As IO.FileStream, ByVal reader As IO.BinaryReader)
        On Error Resume Next

        reader.Close()
        reader = Nothing
        stream.Close()
        stream = Nothing
    End Sub

    ' MD5 Validation and Calculation
    Friend _validated As Boolean
    ''' <summary>
    ''' Returns a value indicating if the current file has already been checked against its MD5 and is valid
    ''' </summary>
    ''' <value></value>
    ''' <returns>True if the file has been checked and is valid, false if isnt checked or isnt valid</returns>
    ''' <remarks></remarks>
    Public ReadOnly Property Validated As Boolean
        Get
            Return _validated
        End Get
    End Property
    Friend _validationPerformed As Boolean
    ''' <summary>
    ''' Returns a value indicating if the current file has checked against its MD5. Doesnt care if match or not
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property ValidationPerformed As Boolean
        Get
            Return _validationPerformed
        End Get
    End Property
    ''' <summary>
    ''' Validates this Stream against its MD5 as stored inside the Arphoon Package
    ''' </summary>
    ''' <returns>True if the file is Valid, False if not</returns>
    ''' <remarks></remarks>
    Public Function Validate() As Boolean
        If CanRead = False Then
            _validated = False
            _validationPerformed = False
            Return False
        End If

        If _length = 0 Then
            _validated = False
            _validationPerformed = True
            Return False
        End If

        Try
            Dim md5Csp As New Security.Cryptography.MD5CryptoServiceProvider

            Dim bHash() As Byte
            Dim hash As String

            Dim clone As Object
            clone = MyBase.MemberwiseClone
            bHash = md5Csp.ComputeHash(clone)

            clone = Nothing

            If bHash Is Nothing Then
                _validated = False
                _validationPerformed = True
                Return False
            End If

            If Not bHash.Length = 16 Then
                _validated = False
                _validationPerformed = True
                Return False
            End If

            Dim i As Integer
            hash = ""
            For i = 0 To bHash.Length - 1
                Dim prefix As String
                prefix = Hex(bHash(i))
                If prefix.Length = 1 Then prefix = "0" & prefix
                hash &= prefix
            Next

            hash = UCase(hash)
            hash = Trim(hash)

            Reset()

            If Trim(UCase(_md5Hash)) = hash Then
                _validated = True
                _validationPerformed = True
                Return True
            Else
                _validated = False
                _validationPerformed = True
                Return False
            End If
        Catch ex As Exception
            _validated = False
            _validationPerformed = True
            Return False
        End Try
    End Function

    ' Readonly Property Overrides
    Public Overrides ReadOnly Property CanRead As Boolean
        Get
            If _closed = True Then Return False
            If _length = 0 Then Return False
            Try
                If _position > _length - 1 Then
                    Return False
                End If

                If _position < 0 Then Return False

                Return True
            Catch ex As Exception
                Return False
            End Try
        End Get
    End Property
    Public Overrides ReadOnly Property CanSeek As Boolean
        Get
            Return Not _closed
        End Get
    End Property
    Public Overrides ReadOnly Property CanTimeout As Boolean
        Get
            Return False
        End Get
    End Property
    Public Overrides ReadOnly Property CanWrite As Boolean
        Get
            Return False
        End Get
    End Property

    ' Primary Procedures and Function Overrides
    Public Overrides Function Seek(ByVal offset As Long, ByVal origin As System.IO.SeekOrigin) As Long
        Select Case origin
            Case IO.SeekOrigin.Begin
                SeekTo(offset, False)
            Case IO.SeekOrigin.Current
                SeekTo(offset, True)
            Case IO.SeekOrigin.End
                ' Calculate the new offset
                Dim newOffset As Long
                newOffset = _length - offset
                SeekTo(offset, False)
            Case Else
                ' Same as Begin
                SeekTo(offset, False)
        End Select

        Return _position
    End Function
    Public Overrides Function Read(<Runtime.InteropServices.In(), Runtime.InteropServices.Out()> ByVal buffer() As Byte, ByVal arrayOffset As Integer, ByVal count As Integer) As Integer
        ' WARNING: This function is designed to work with .NET methods ONLY. Specifically MD5 hashing functions
        If CanRead = False Then Return -1

        If count = 0 Then Return 0
        If _length = 0 Then Return -1

        Dim xbuffer() As Byte
        'Dim prevPosition As Long = Position ' Store the current position

        'SeekTo(offset, False) ' Seek to offset
        xbuffer = ReadBytes(count) ' Read data
        Array.ConstrainedCopy(xbuffer, 0, buffer, arrayOffset, xbuffer.Length) ' Update buffer

        'SeekTo(prevPosition, False) ' Restore previous position

        Return xbuffer.Length
    End Function
    Public Function ReadBytesFromOffset(ByRef buffer() As Byte, ByVal fileOffset As Integer, ByVal count As Integer) As Integer
        ' WARNING: This function is NOT designed to work with .NET methods like MD5 hashing functions

        If CanRead = False Then Return -1
        If _length = 0 Then Return -1

        If count = 0 Then Return 0

        Dim xBuffer() As Byte
        'Dim prevPosition As Long = Position ' Store the current position

        SeekTo(fileOffset, False) ' Seek to offset
        xBuffer = ReadBytes(count) ' Read data

        Array.ConstrainedCopy(xBuffer, 0, buffer, 0, xBuffer.Length) ' Update buffer

        'SeekTo(prevPosition, False) ' Restore previous position

        Return xBuffer.Length
    End Function

    ' Unsupported Methods
    Public Overrides Sub Flush()
        Debug.Print("Call to unsupported method: Flush()")
    End Sub
    Public Overrides Sub Write(ByVal buffer() As Byte, ByVal offset As Integer, ByVal count As Integer)
        Debug.Print("Call to unsupported method: Write()")
    End Sub
    Public Overrides Sub WriteByte(ByVal value As Byte)
        Debug.Print("Call to unsupported method: Write()")
    End Sub
    Public Overrides Sub SetLength(ByVal value As Long)
        Debug.Print("Call to unsupported method: SetLength()")
    End Sub

    Protected Overrides Sub Finalize()
        If Closed = False Then
            Close()
        End If
        MyBase.Finalize()
    End Sub
End Class


